iT邦幫忙

2022 iThome 鐵人賽

DAY 28
0
Mobile Development

Kotlin 實踐 Material Design 懶人包系列 第 28

Day28 使用M3的Motion - Transition Patterns四種模式,其中的Container transform patterns (1)

  • 分享至 

  • xImage
  •  

Material Design的Motion由四種模式組成,用於在元件或全屏檢視之間過渡轉換動畫,這些模式主要在透過加強使用者介面元素之間的關係來幫助使用者瀏覽和理解應用程式,過渡轉換是一個協調的動畫序列,序列中的UI元素被歸類為傳出、傳入或持久,在介面適應時保持使用者的焦點。

Transition(過渡) patterns

  1. Container transform
  2. Shared axis
  3. Fade through
  4. Fade

Selecting a pattern

  1. Container transform :頁面上主要功能和常用功能始終在畫面上的常駐功能,如Button、listView、Cards,在螢幕上開始和關閉的畫面部份
  2. shared axis:使用在頁面切換,例如垂直或水平layout中的元素,或者元素之間可能存在導航關係。
  3. Fade through:當元素之間的關係較無重要因素,漸變模式可以應用於UI元素,而彼此之間沒有牢固的關係,例如底部導航中的目的地。
  4. Fade:應用於進入或退出螢幕的UI元素,例如Dialogs。

library支援限制:

  1. AndroidX Transition library (androidx.transition)

    • 可在com.google.android.material.transition
    • 支援API級別14+
    • 支援Fragments和View,但不支援 Activities 或 Windows
    • 包含反向移植的錯誤修復和跨API級別的一致行為
  2. Android Transition Framework (android.transition):

    • 可在com.google.android.material.transition.platform
    • 支援API級別21+
    • 支援Fragments、Views、Activities、Windows
    • 錯誤修復了未反向移植,並且可能在API級別之間有不同的行為

實作上探討

app module's build.gradle dependencies.
MDC-Android library implementation 'com.google.android.material:material:1.2.0'

Container transform

此模式在兩個UI元素之間建立可見的動畫效果,MaterialContainerTransform是一個shared element transition(共享元素過渡),設計應用程式中的Activity過渡透過共同元素之間的連動和轉換,在不同狀態之間提供視覺動畫效果。

「共享元素指」的是start View或ViewGroup,然後將其大小和形狀轉換為end View或ViewGroup,這些開始和結束View或ViewGroup的“共享元素”在轉換時,它們的內容被交換以建立過渡動畫。

Transition between activities examples

Activity 和 Window過渡需要使用com.google.android.material.transition.platform提供的Android框架Transition,並且只能在API級別21及更高版本上可用。

準備兩個Class,和兩個 layout
Activity A’s Layout:
在Activity的layout中,識別要用作容器轉換概述中描述的“共享元素”的開始View。 給View一個Transition名稱android:transitionName="shared_element_container"

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:transitionName="shared_element_container">

    <Button
        android:id="@+id/btnTOB"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="點擊放大圖案"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <View
        android:id="@+id/start_view"
        android:layout_width="150dp"
        android:layout_height="150dp"
        android:layout_marginTop="20dp"
        android:background="@drawable/ic_emoji_symbols_24"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/btnTOB" />

</androidx.constraintlayout.widget.ConstraintLayout>

Activity A’s Class
在onCreate做相關設定

  1. 設定Enable Activity Transitions
  2. 附加用於捕獲此活動的共享元素的Callback
  3. 在整個過渡過程中保持系統欄(狀態欄、導航欄)的永續性。
  4. 透過 ActivityOptions.makeSceneTransitionAnimation()傳遞Bundle和指定的動畫效果的view,和要與Activity B 中要匹配的轉換View的transitionName名稱
class TransitionActivityA: AppCompatActivity() {

    private lateinit var binding: ActivityABinding

    override fun onCreate(savedInstanceState: Bundle?) {
		// 1.設定Enable Activity Transitions
		window.requestFeature(Window.FEATURE_ACTIVITY_TRANSITIONS)
       
        // 2. 附加用於捕獲此活動的共享元素的Callback
        setExitSharedElementCallback(MaterialContainerTransformSharedElementCallback())
        
		// 3. 在整個過渡過程中保持系統欄(狀態欄、導航欄)的永續性。
        window.sharedElementsUseOverlay = false

        super.onCreate(savedInstanceState)
        binding = ActivityABinding.inflate(layoutInflater)
        setContentView(binding.root)

		// button點擊
        binding.btnTOB.setOnClickListener {
			// Activity B 中要匹配的轉換名稱
            val intent = Intent(this, TransitionActivityB::class.java)
            // 透過 ActivityOptions.makeSceneTransitionAnimation()傳遞Bundle和指定的動畫效果的view
            val options = ActivityOptions.makeSceneTransitionAnimation(
                this, binding.startView, "shared_element_container") 
            startActivity(intent, options.toBundle())
        }
    }
}

Activity B’s Layout:

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    
    <ImageView
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:background="@drawable/ic_emoji_symbols_24"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>

Activity B’s Class:
在onCreate做相關設定

  1. content 設定transition name, 開始跟 Activity A 的視圖轉換名稱
  2. 加上 callback 用來接收Activity A的 container transform
  3. 將此 Activity 的進入動畫和返迴動畫,轉換設置為 MaterialContainerTransform
    • sharedElementEnterTransition 進入動畫
    • sharedElementReturnTransition 返迴動畫
    • addTarget(android.R.id.content) 設定是window’s root 這將可以讓ActivityA的開始View開啟到ActivityB的全屏效果
class TransitionActivityB: AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        // 1. content設定transition name, 開始跟 Activity A 的視圖轉換名稱
        findViewById<View>(android.R.id.content).transitionName = "shared_element_container"

        // 2. 加上 callback 用來接收Activity A的 container transform
        setEnterSharedElementCallback(MaterialContainerTransformSharedElementCallback())

        // 3. 將此 Activity 的進入和返迴轉換設置為 MaterialContainerTransform
        window.sharedElementEnterTransition = MaterialContainerTransform().apply {
            addTarget(android.R.id.content)
            duration = 500L
        }
        window.sharedElementReturnTransition = MaterialContainerTransform().apply {
            addTarget(android.R.id.content)
            duration = 250L
        }

        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_b)

    }
}

Transition between Fragments examples

準備Fragment A and Fragment B's layouts、Class
Transition名稱android:transitionName="shared_element_container"

<!--fragment_a.xml-->
<View
		android:id="@+id/start_view"
		android:transitionName="shared_element_container" />
<!--fragment_b.xml-->
<View
	  android:id="@+id/end_view"
	  android:transitionName="shared_element_container" />

FragmentA 在 onCreate method 設定
畫面的共享元素的動畫轉換,將FragmentB新增到/替換Fragment容器之前完成。

class FragmentTransitionA: Fragment() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val fragmentB = FragmentTransitionB()
        fragmentB.sharedElementEnterTransition = MaterialContainerTransform()
    }
}

// 或者也可設定在Fragment B's onCreate method
class FragmentTransitionB: Fragment() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
					sharedElementEnterTransition= MaterialContainerTransform()
    }
}

FragmentA‘s Class:
新增或替換FragmentB,將共享元素從開始場景新增到Fragment。

 childFragmentManager
   .beginTransaction()
    // 透過transitionName找到 FragmentA 中的 start View 和 FragmentB 中 end View 的 transitionName
   .addSharedElement(view, "shared_element_container")
   .replace(R.id.fragment_container, fragmentB, "FragmentTransitionB")
   .addToBackStack("FragmentTransitionB")
   .commit()         

執行此Transition時,會注意到轉換開始,Fragment A就會消失。 這是因為FragmentA已從容器中移除。 要在容器轉換播放時“保持”FragmentA,將FragmentA的退出Transition設定為保留。

  1. 設定Hold()
  2. 設定 MaterialElevationScale
    • 適用於 API level 21+、設定 android:transitionGroup="true"
// 1. 設定Hold()
exitTransition = Hold()

// 2. 設定 MaterialElevationScale
// MaterialElevationScale(false),在進入轉換Fragment時退出縮小FragmentA
exitTransition = MaterialElevationScale(false)
// 在返回轉換Fragment期間重新進入FragmentA時, MaterialElevationScale(true),以放大FragmentA。
reenterTransition = MaterialElevationScale(true)

完成FragmentA‘s Class程式碼

class FragmentTransitionA: Fragment() {

    private var _binding : FragmentTransitionABinding? = null
    private val binding get() = _binding
    private val fragmentB = FragmentTransitionB()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        fragmentB.sharedElementEnterTransition = MaterialContainerTransform()
        exitTransition = Hold()
    }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        _binding = FragmentTransitionABinding.inflate(layoutInflater, container, false)
        return binding?.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        childFragmentManager
            .beginTransaction()
            // 透過transitionName找到 FragmentA 中的 start View 和 FragmentB 中 end View 的 transitionName
            .addSharedElement(view, "shared_element_container")
            .replace(R.id.fragment_container, fragmentB, "FragmentTransitionB")
            .addToBackStack("FragmentTransitionB")
            .commit()
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}

Customizing the container transform

可以在MaterialContainerTransform上手動設定以下屬性,以自定義動畫的外觀和感覺:

Container transform attributes
https://ithelp.ithome.com.tw/upload/images/20221012/2014446911hPK1CvVL.png

Container transform properties

設定動畫轉換的背景顏色開始畫面和結束的畫面
https://ithelp.ithome.com.tw/upload/images/20221012/20144469CKs91mc0j2.png

感謝耐心看到這邊~~/images/emoticon/emoticon01.gif
今日只講了Motion - Container transform patterns,明日繼續其他。

參考資料:Material Design Motion


上一篇
Day27 使用M3的Radio button
下一篇
Day29 使用M3的Motion - Shared axis(2)
系列文
Kotlin 實踐 Material Design 懶人包30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言